Skip to content

fix(tsconfig): support transitive project references for paths resolution#213

Open
stormslowly wants to merge 5 commits into
mainfrom
feat/tsconfig-transitive-references
Open

fix(tsconfig): support transitive project references for paths resolution#213
stormslowly wants to merge 5 commits into
mainfrom
feat/tsconfig-transitive-references

Conversation

@stormslowly
Copy link
Copy Markdown
Collaborator

@stormslowly stormslowly commented May 13, 2026

Summary

Make rspack-resolver's tsconfig project-reference handling match tsc's "nearest tsconfig wins" semantics and enhanced-resolve's recursive references: "auto" walk.

Three bugs are fixed:

  1. Transitive references not walked. When the entry tsconfig declares references: [B] and B declares references: [C], a file inside C should resolve via C's own paths. Previously, only directly-listed references were loaded (one level deep) and TsConfig::resolve only iterated those, so requests from a transitively-referenced project's directory silently fell back to the entry tsconfig's paths.
  2. extends not applied to referenced configs. A referenced project whose baseUrl/paths come from extends "../tsconfig.base.json" resolved as if it had no aliases. Pre-existing latent bug at depth 1 that the new transitive support makes easier to hit.
  3. Cycles cause stack overflow. A pair of project references that point at each other (a → b → a) caused infinite recursion when references: "auto" recursively walked the graph.

Behavior comparison (file at c/src/foo.ts, chain a → b → c, entry a/tsconfig.json)

Resolver Paths used
tsc (nearest tsconfig wins) c/tsconfig.json
enhanced-resolve references: "auto" (recursive) c/tsconfig.json
rspack-resolver before this PR a/tsconfig.json
rspack-resolver after this PR c/tsconfig.json

Changes

  • src/lib.rs
    • Extract load_references from load_tsconfig. The helper recursively loads each reference's own references so transitive paths are honored.
    • Extract merge_tsconfig_extends; call it on referenced tsconfigs (inside load_references) so a reference inherits its base config's baseUrl/paths before its own references are walked.
    • Thread a visited: &mut FxHashSet<PathBuf> through load_references. Each tsconfig inserts its own path before walking its references; when a candidate reference is already in the chain, the cycle edge is cut (the reference is still attached so its paths are honored, but its nested references are not walked). Direct self-reference (A → A) detection via the existing equality check inside the cache callback is preserved.
  • src/tsconfig.rs: TsConfig::resolve recursively descends into nested references via a new find_reference_paths helper, returning the nearest reference whose base_path contains the requested path before falling back to the current tsconfig's own paths.
  • Fixtures:
    • references-transitive/ — three-level chain app → project_b → project_c.
    • references-extends/ — single-level reference whose paths come from extends "../tsconfig.base.json".
    • references-cycle/a ↔ b pair.
  • Tests added in tsconfig_project_references:
    • transitive_references — covers all three levels of the chain.
    • references_with_extends — covers extends inheritance through a reference.
    • cyclic_references — asserts resolution succeeds from both sides of an a ↔ b cycle without stack overflow.

Test plan

  • cargo test --all-features --lib -- --skip pnp — 125 passed (3 new tests + 122 existing, including auto, manual, disabled, self_reference, tsconfig_file_as_file_dependencies)
  • cargo clippy --all-features -- --deny warnings clean
  • cargo fmt --check clean
  • PnP tests fail both before and after this change (pre-existing fixture-install issue, unrelated)

When the entry tsconfig declares `references: [B]` and B declares
`references: [C]`, a file inside C should resolve via C's own `paths`,
matching `tsc`'s "nearest tsconfig wins" semantics and webpack's
recursive `references: "auto"` walk.

Previously, rspack-resolver loaded only the directly-listed references
(one level deep) and `TsConfig::resolve` only iterated those direct
references. As a result, requests from a transitively-referenced
project's directory would silently fall back to the entry tsconfig's
`paths` and resolve incorrectly.

Changes:
- `load_tsconfig` extracts `load_references` which recursively loads
  nested project references. Existing self-reference detection (A → A)
  is preserved; cycle detection across multiple levels is intentionally
  out of scope for this change.
- `TsConfig::resolve` recursively descends into nested references via
  `find_reference_paths`, returning the nearest reference whose
  `base_path` contains the requested path before falling back to the
  current tsconfig.
Copilot AI review requested due to automatic review settings May 13, 2026 07:29
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 28e11ff3d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib.rs Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes TypeScript tsconfig project reference handling so paths resolution honors transitive references (A → B → C), matching TypeScript’s “nearest tsconfig wins” behavior and enhanced-resolve’s recursive reference walk.

Changes:

  • Recursively load referenced tsconfigs so nested reference configs (and their paths) are available during resolution.
  • Update TsConfig::resolve to recursively search nested references and use the nearest matching reference config for paths.
  • Add a new fixture and test covering a 3-level transitive reference chain.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/lib.rs Extracts and adds recursive load_references to load nested project references.
src/tsconfig.rs Adds recursive reference lookup (find_reference_paths) used by TsConfig::resolve.
src/tests/tsconfig_project_references.rs Adds transitive_references test validating behavior across 3 reference levels.
fixtures/tsconfig/cases/references-transitive/app/tsconfig.json Adds entry project tsconfig for transitive reference fixture.
fixtures/tsconfig/cases/references-transitive/app/aliased/index.ts Adds aliased target file for app in fixture.
fixtures/tsconfig/cases/references-transitive/project_b/tsconfig.json Adds middle project tsconfig referencing project_c.
fixtures/tsconfig/cases/references-transitive/project_b/src/aliased/index.ts Adds aliased target file for project_b in fixture.
fixtures/tsconfig/cases/references-transitive/project_c/tsconfig.json Adds leaf project tsconfig defining its own paths.
fixtures/tsconfig/cases/references-transitive/project_c/aliased/index.ts Adds aliased target file for project_c in fixture.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib.rs Outdated
Comment thread src/tsconfig.rs
Comment on lines +184 to 195
fn find_reference_paths(&self, path: &Path, specifier: &str) -> Option<Vec<PathBuf>> {
for tsconfig in self
.references
.iter()
.filter_map(|reference| reference.tsconfig.as_ref())
{
if let Some(nested) = tsconfig.find_reference_paths(path, specifier) {
return Some(nested);
}
if path.starts_with(tsconfig.base_path()) {
return tsconfig.resolve_path_alias(specifier);
return Some(tsconfig.resolve_path_alias(specifier));
}
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 13, 2026

Merging this PR will not alter performance

✅ 12 untouched benchmarks


Comparing feat/tsconfig-transitive-references (23b8954) with main (8975e38)

Open in CodSpeed

When a project reference uses `extends` to inherit its `baseUrl`/`paths`
from a shared base config, those fields must be merged before the
reference is consulted for resolution. The previous flow loaded each
reference through `cache.tsconfig` and only ran the extends-merging
logic on the entry tsconfig, so a transitively-referenced project
whose aliases live in an extended base file would resolve as if it had
no aliases at all.

This was a pre-existing latent bug at depth 1 — direct references with
`extends` did not inherit their aliases either — that the new
transitive-references support made easier to hit.

Changes:
- Extract `merge_tsconfig_extends` from `load_tsconfig` and call it
  from `load_references`' inner callback after the self-reference
  check, before recursing into nested references.
- Update the `references-transitive` fixture: `project_c` now reads its
  `paths` from a sibling `tsconfig.base.json` via `extends`. The
  existing `transitive_references` test fails before this fix
  (NotFound) and passes after.

Detected via Codex review of #213.
Add a dedicated test (`references_with_extends`) and fixture
(`references-extends/`) that exercises the fix from the previous
commit in isolation: a single-level reference whose `baseUrl`/`paths`
are inherited via `extends "../tsconfig.base.json"`.

Without `merge_tsconfig_extends` being applied to the reference, the
test fails with `NotFound` — confirming the test would have caught
the latent extends-on-reference bug.

Also revert the `references-transitive` fixture so it focuses purely
on multi-level walking; the extends-merging behavior is now covered
by the new fixture and test.
A pair of project references that point at each other (a → b → a)
previously caused infinite recursion when `references: "auto"`
recursively loaded the graph, blowing the test thread's stack.

Add a `visited` set threaded through `load_references`. Each tsconfig
inserts its own path before walking its references. When a candidate
reference's loaded path is already in the chain, the cycle edge is
cut: the reference is still attached so its own `paths` are honored,
but its nested references are not walked.

Includes a `references-cycle` fixture (a ↔ b) and a `cyclic_references`
test that asserts resolution succeeds from both sides without stack
overflow.

Direct self-reference (A → A) detection via the existing equality
check inside the cache callback is preserved.
@stormslowly stormslowly changed the title fix(tsconfig): honor transitive project references for paths resolution fix(tsconfig): support transitive project references for paths resolution May 13, 2026
@stormslowly stormslowly requested a review from hardfist May 13, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants